package org.zoomdev.zoom.ioc.impl;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.zoomdev.zoom.common.Destroyable;
import org.zoomdev.zoom.common.caster.Caster;
import org.zoomdev.zoom.common.config.ConfigReader;
import org.zoomdev.zoom.common.utils.CachedClasses;
import org.zoomdev.zoom.common.utils.Classes;
import org.zoomdev.zoom.common.utils.RuntimeUtils;
import org.zoomdev.zoom.ioc.Ioc;
import org.zoomdev.zoom.ioc.IocConfigKey;
import org.zoomdev.zoom.ioc.IocConstructor;
import org.zoomdev.zoom.ioc.IocEnhancer;
import org.zoomdev.zoom.ioc.IocException;
import org.zoomdev.zoom.ioc.IocFactory;
import org.zoomdev.zoom.ioc.IocHolder;
import org.zoomdev.zoom.ioc.IocKey;
import org.zoomdev.zoom.ioc.IocTypeKey;
import org.zoomdev.zoom.ioc.IocUtils;
import org.zoomdev.zoom.ioc.annotations.Config;
import org.zoomdev.zoom.ioc.annotations.Inject;
import org.zoomdev.zoom.ioc.impl.factories.ClassIocFactory;
import org.zoomdev.zoom.ioc.impl.factories.InterfaceIocFactory;
import org.zoomdev.zoom.ioc.modules.CoreIocModule;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;

public final class ZoomIoc implements Ioc, Destroyable {

    private static final Logger log = LoggerFactory.getLogger(CoreIocModule.class);

    private final Map<IocKey, Object> container = new HashMap<IocKey, Object>();
    private final List<IocFactory> factories = new ArrayList<>();
    private final Map<IocKey, IocConstructor> constructorMap = new HashMap<>();
    private IocEnhancer enhancer;

    public ZoomIoc() {
        ClassIocFactory factory = new ClassIocFactory();
        addFactory(new InterfaceIocFactory(factory));
        addFactory(factory);
        put(new IocTypeKey(Ioc.class), this);
        RuntimeUtils.addShutDownHook(() -> destroy());
    }

    @Override
    public void destroy() {
        synchronized (this) {
            log.info("==============Ioc destroy==============");
            Collection collection = container.values();
            HashSet set = new HashSet();
            for(Object data : collection){
                if(data==null){
                    continue;
                }
                if(data == this){
                    continue;
                }
                set.add(data);
            }
            //去重
            for (Object i : set) {
                Classes.destroy(i);
            }
            container.clear();
        }
    }

    public <T> T fetch(Type type, String name) {
        type = IocUtils.createGenericType(type);
        return fetch(new IocTypeKey(type, name));
    }

    @Override
    public <T> T fetch(Type type) {
        return fetch(type, null);
    }

    @Override
    public void addFactory(IocFactory factory) {
        assert (factory != null);
        synchronized (this) {
            put(new IocTypeKey(factory.getClass()), factory);
            factories.add(factory);
        }

    }

    @Override
    public void insertFactory(IocFactory factory) {
        assert (factory != null);
        synchronized (this) {
            IocKey key = new IocTypeKey(factory.getClass());
            put(key, factory);
            factories.add(0, factory);
        }
    }

    @Override
    public void replaceFactory(Class oldFactory, IocFactory factory) {
        synchronized (this) {
            for (int i = 0, c = factories.size(); i < c; ++i) {
                IocFactory f = factories.get(i);
                if (f.getClass() == oldFactory) {
                    factories.set(i, factory);
                    break;
                }
            }
        }
    }

    @Override
    public void put(IocKey key, Object data) {
        container.put(key, data);
    }

    @Override
    public Collection<IocKey> find(Class<?> type) {
        synchronized (this) {
            for (int i = 0, c = factories.size(); i < c; ++i) {
                IocFactory f = factories.get(i);
                if (f instanceof IocFactory.IocKeyContainer) {
                    Collection collection = ((IocFactory.IocKeyContainer) f).find(type);
                    if (collection != null) {
                        return collection;
                    }
                }
            }
        }
        return null;
    }

    @Override
    public Collection<Object> gets(Class<?> type) {
        return container.values().stream().filter(o -> Classes.instanceOf(o, type)).collect(Collectors.toList());
    }

    @Override
    public Collection<Object> gets(Predicate<Object> predicate) {
        return container.values().stream().filter(predicate).collect(Collectors.toList());
    }

    @Override
    public Collection<IocKey> getDependencies(IocKey key) {
        Set<IocKey> set = new HashSet<>();
        getDependencies(new IocKey[]{key}, set);
        return set;
    }

    @Override
    public Collection<IocKey> getLinked(IocKey key) {
        return null;
    }

    public <T> T fetch(IocKey key) {
        synchronized (this) {
            Object value = container.get(key);
            if (value != null) {
                return (T) value;
            }
            //进入构建空间
            return (T) create(null, key, new IocCreateContext());
        }
    }

    @Override
    public <T> T get(IocKey key) {
        return (T) container.get(key);
    }

    public void inject(Object result) {
        synchronized (this) {
            IocCreateContext ctx = new IocCreateContext();
            inject(null, result, ctx);
        }
    }

    @Override
    public void setEnhancer(IocEnhancer enhancer) {
        this.enhancer = enhancer;
    }

    void getDependencies(IocKey[] keys, Set<IocKey> set) {
        for (IocKey key : keys) {
            if (key instanceof IocConfigKey) {
                continue;
            }
            IocConstructor iocConstructor = getConstructor(key);
            if (iocConstructor == null) {
                set.add(key);
                continue;
            }
            IocKey[] subKeys = iocConstructor.getKeys(key);
            Collections.addAll(set, subKeys);
            getDependencies(subKeys, set);
        }
    }

    protected IocConstructor getConstructor(IocKey key) {
        IocConstructor constructor = constructorMap.get(key);
        if (constructor != null) {
            return constructor;
        }

        for (IocFactory factory : factories) {
            constructor = factory.createConstructor(key);
            if (constructor != null) {
                if (enhancer != null) {
                    constructor = enhancer.enhance(constructor);
                }
                constructorMap.put(key, constructor);
                return constructor;
            }
        }
        return null;
    }

    private Object create(Object lastConstructor, IocKey key, IocCreateContext ctx) {
        if (log.isDebugEnabled()) {
            log.debug("Create ioc object: " + key);
        }
        ctx.addConstructing(key);

        IocConstructor constructor = getConstructor(key);
        if (constructor == null) {
            String info;
            if (lastConstructor instanceof IocConstructor) {
                info = lastConstructor.toString();
            } else if (lastConstructor == null) {
                info = " root object";
            } else {
                info = lastConstructor.getClass().toString();
            }
            throw new IocException("创建ioc对象失败，找不到" + key + "的构造器,Ioc无法知道怎么创建对象,构造器:" + info);
        }

        IocKey[] keys = constructor.getKeys(key);
        Object[] args = new Object[keys.length];
        for (int i = 0; i < keys.length; ++i) {
            IocKey rk = keys[i];
            Object value;
            if (rk instanceof IocConfigKey) {
                value = rk.getValue(this);
            } else {
                value = container.get(rk);
                if (value == null) {
                    if (ctx.isConstructing(rk)) {
                        throw new IocException("循环依赖" + key + "=>" + rk);
                    }
                    value = create(constructor, rk, ctx);
                }
            }
            args[i] = value;
        }

        Object result;
        try {
            result = constructor.newInstance(key, args);
        } catch (Throwable e) {
            throw new IocException("创建ioc对象失败: KEY:" +
                    key +
                    " 构造器:" +
                    constructor, Classes.getCause(e));
        }
        if (result == null) {
            throw new IocException("返回对象为null");
        }
        ctx.removeConstructing(key, result);
        container.put(key, result);

        try {
            inject(key, result, ctx);
            if (result instanceof IocHolder) {
                ((IocHolder) result).setIoc(this);
            }
        } catch (IocException e) {
            container.remove(key);
            throw e;
        }


        return result;
    }

    private void inject(IocKey key, Object result, IocCreateContext ctx) {
        Class<?> type = result.getClass();
        if (type.getName().startsWith("java")) {
            return;
        }
        Field[] fields = CachedClasses.getFields(type);
        for (Field field : fields) {
            try {
                injectField(key, result, field, ctx);
            } catch (Throwable t) {
                throw new IocException("注入字段失败" + field, Classes.getCause(t));
            }
        }
    }

    private void injectField(IocKey key, Object obj, Field field, IocCreateContext ctx) throws Throwable {
        Inject inject = field.getAnnotation(Inject.class);
        if (inject != null) {
            if (Modifier.isFinal(field.getModifiers())) {
                log.warn("字段" + field + "为final，无法注入");
                return;
            }

            //判断是否是配置
            if (StringUtils.isEmpty(inject.config())) {
                Type type = field.getGenericType();
                type = IocKey.getRealTypeOfField(type, key);
                IocKey iocKey = new IocTypeKey(type, inject.name());
                Object needsObj = container.get(iocKey);
                if (needsObj == null) {
                    if (ctx.isConstructing(iocKey)) {
                        ctx.addField(iocKey, field, obj);
                        return;
                    }
                    needsObj = create(obj, iocKey, ctx);
                }
                field.set(obj, needsObj);
                return;
            }
            Object value = ConfigReader.getDefault().get(inject.config());
            if (value == null) {
                return;
            }
            field.set(obj, Caster.toType(value, field.getGenericType()));
            return;
        }

        Config config = field.getAnnotation(Config.class);
        if (config != null) {
            Object value = ConfigReader.getDefault().getObject(config.key(), config.file());
            if (value == null) {
                return;
            }
            field.set(obj, Caster.toType(value, field.getGenericType()));
        }
    }


}
